{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Spatial Joins\n",
    "\n",
    "A *spatial join* uses [binary predicates](http://shapely.readthedocs.io/en/latest/manual.html#binary-predicates) \n",
    "such as `intersects` and `crosses` to combine two `GeoDataFrames` based on the spatial relationship \n",
    "between their geometries.\n",
    "\n",
    "A common use case might be a spatial join between a point layer and a polygon layer where you want to retain the point geometries and grab the attributes of the intersecting polygons.\n",
    "\n",
    "![illustration](https://web.natur.cuni.cz/~langhamr/lectures/vtfg1/mapinfo_1/about_gis/Image23.gif)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## Types of spatial joins\n",
    "\n",
    "We currently support the following methods of spatial joins. We refer to the *left_df* and *right_df* which are the correspond to the two dataframes passed in as args.\n",
    "\n",
    "### Left outer join\n",
    "\n",
    "In a LEFT OUTER JOIN (`how='left'`), we keep *all* rows from the left and duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the right if they intersect and lose right rows that don't intersect. A left outer join implies that we are interested in retaining the geometries of the left. \n",
    "\n",
    "This is equivalent to the PostGIS query:\n",
    "```\n",
    "SELECT pts.geom, pts.id as ptid, polys.id as polyid  \n",
    "FROM pts\n",
    "LEFT OUTER JOIN polys\n",
    "ON ST_Intersects(pts.geom, polys.geom);\n",
    "\n",
    "                    geom                    | ptid | polyid \n",
    "--------------------------------------------+------+--------\n",
    " 010100000040A9FBF2D88AD03F349CD47D796CE9BF |    4 |     10\n",
    " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     10\n",
    " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     20\n",
    " 0101000000F0D88AA0E1A4EEBF7052F7E5B115E9BF |    2 |     20\n",
    " 0101000000818693BA2F8FF7BF4ADD97C75604E9BF |    1 |       \n",
    "(5 rows)\n",
    "```\n",
    "\n",
    "### Right outer join\n",
    "\n",
    "In a RIGHT OUTER JOIN (`how='right'`), we keep *all* rows from the right and duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the left if they intersect and lose left rows that don't intersect. A right outer join implies that we are interested in retaining the geometries of the right. \n",
    "\n",
    "This is equivalent to the PostGIS query:\n",
    "```\n",
    "SELECT polys.geom, pts.id as ptid, polys.id as polyid  \n",
    "FROM pts\n",
    "RIGHT OUTER JOIN polys\n",
    "ON ST_Intersects(pts.geom, polys.geom);\n",
    "\n",
    "  geom    | ptid | polyid \n",
    "----------+------+--------\n",
    " 01...9BF |    4 |     10\n",
    " 01...9BF |    3 |     10\n",
    " 02...7BF |    3 |     20\n",
    " 02...7BF |    2 |     20\n",
    " 00...5BF |      |     30\n",
    "(5 rows)\n",
    "```\n",
    "\n",
    "### Inner join\n",
    "\n",
    "In an INNER JOIN (`how='inner'`), we keep rows from the right and left only where their binary predicate is `True`. We duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the right and left only if they intersect and lose all rows that do not. An inner join implies that we are interested in retaining the geometries of the left. \n",
    "\n",
    "This is equivalent to the PostGIS query:\n",
    "```\n",
    "SELECT pts.geom, pts.id as ptid, polys.id as polyid  \n",
    "FROM pts\n",
    "INNER JOIN polys\n",
    "ON ST_Intersects(pts.geom, polys.geom);\n",
    "\n",
    "                    geom                    | ptid | polyid \n",
    "--------------------------------------------+------+--------\n",
    " 010100000040A9FBF2D88AD03F349CD47D796CE9BF |    4 |     10\n",
    " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     10\n",
    " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     20\n",
    " 0101000000F0D88AA0E1A4EEBF7052F7E5B115E9BF |    2 |     20\n",
    "(4 rows) \n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Spatial Joins between two GeoDataFrames\n",
    "\n",
    "Let's take a look at how we'd implement these using `GeoPandas`. First, load up the NYC test data into `GeoDataFrames`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "from shapely.geometry import Point\n",
    "from geopandas import GeoDataFrame, read_file\n",
    "import geodatasets\n",
    "\n",
    "# NYC Boros\n",
    "zippath = geodatasets.get_path(\"nybb\")\n",
    "polydf = read_file(zippath)\n",
    "\n",
    "# Generate some points\n",
    "b = [int(x) for x in polydf.total_bounds]\n",
    "N = 8\n",
    "pointdf = GeoDataFrame(\n",
    "    [\n",
    "        {\"geometry\": Point(x, y), \"value1\": x + y, \"value2\": x - y}\n",
    "        for x, y in zip(\n",
    "            range(b[0], b[2], int((b[2] - b[0]) / N)),\n",
    "            range(b[1], b[3], int((b[3] - b[1]) / N)),\n",
    "        )\n",
    "    ]\n",
    ")\n",
    "\n",
    "# Make sure they're using the same projection reference\n",
    "pointdf.crs = polydf.crs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "polydf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointdf.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "polydf.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Joins"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "join_left_df = pointdf.sjoin(polydf, how=\"left\")\n",
    "join_left_df\n",
    "# Note the NaNs where the point did not intersect a boro"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "join_right_df = pointdf.sjoin(polydf, how=\"right\")\n",
    "join_right_df\n",
    "# Note Staten Island is repeated"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "join_inner_df = pointdf.sjoin(polydf, how=\"inner\")\n",
    "join_inner_df\n",
    "# Note the lack of NaNs; dropped anything that didn't intersect"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We're not limited to using the `intersection` binary predicate. Any of the `Shapely` geometry methods that return a Boolean can be used by specifying the `predicate` kwarg."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointdf.sjoin(polydf, how=\"left\", predicate=\"within\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can also conduct a nearest neighbour join with `sjoin_nearest`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointdf.sjoin_nearest(polydf, how=\"left\", distance_col=\"Distances\")\n",
    "# Note the optional Distances column with computed distances between each point\n",
    "# and the nearest polydf geometry."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
